Conversation
Stock CircuitPython BLE peripherals can only do legacy "Just Works" pairing: nothing surfaces a passkey or numeric-comparison value to Python, and the espressif port additionally rejects *_WITH_MITM characteristic permissions outright. A peripheral with a display and a button (a lock, a medical device) therefore can't require an authenticated bond. Add numeric-comparison pairing to _bleio. NimBLE already ships LE Secure Connections crypto, so the espressif port is a config change rather than a crypto port; nordic, silabs and zephyr-cp get stubs. shared-bindings/_bleio/Connection: - authenticated: True when the link is MITM-protected (numeric comparison or passkey entry completed), not merely encrypted. Ports without an implementation always report False. - pairing_numeric_comparison: the pending 6-digit value, or None. Non-blocking - the peer drives the SM procedure. - confirm_pairing(accept): answer it. Raises ConnectionError / BluetoothError rather than silently doing nothing on misuse. espressif: - The adapter keeps the legacy Just Works defaults (NO_IO, sm_mitm = 0) until a characteristic or descriptor is constructed with a *_WITH_MITM permission, at which point bleio_adapter_enable_mitm_pairing() raises sm_io_cap to DISPLAY_YESNO and sm_mitm to 1. Existing headless peripherals are unaffected. sm_sc is now always 1 (negotiated per pairing, strictly better, and numeric comparison requires it). - Characteristic.c / Descriptor.c stop raising NotImplementedError for *_WITH_MITM and map the MITM / LESC-MITM / SIGNED modes to NimBLE's _AUTHEN flags. - Connection.c: a PAIR_WAITING_NUMCMP state holds the value from BLE_GAP_EVENT_PASSKEY_ACTION; confirm_pairing() injects the answer with ble_sm_inject_io(). A passkey action this device can't service (INPUT / OOB) terminates the link instead of stalling SM. nordic / silabs / zephyr-cp: get_authenticated() returns False; the other two entry points raise NotImplementedError.
Ports using devices/ble_hci/common-hal/_bleio (atmel-samd samd51, mimxrt10xx, raspberrypi, stm, broadcom, cxd56) were missing common_hal_bleio_connection_get_authenticated, common_hal_bleio_connection_get_pairing_numeric_comparison, and common_hal_bleio_connection_confirm_pairing, causing link failures. Add the same not-implemented stubs already used by nordic, silabs, and zephyr-cp.
tannewt
left a comment
There was a problem hiding this comment.
Cool! Thanks for working on this!
tannewt reviewed PR adafruit#11340 and asked for a few changes: - Rename pairing_numeric_comparison to numeric_comparison, since the pairing_ prefix was redundant on a Connection property already about pairing. Borrows the term directly from the Bluetooth spec's "Numeric Comparison" association model. - Drop the "Only implemented on the espressif port." line from confirm_pairing()'s docstring: numeric_comparison already reads None on ports without this feature, so a correct polling loop never calls confirm_pairing() there, and calling it anyway already raises NotImplementedError. - Stop passing a custom message to that NotImplementedError - the raise already makes it obvious via the function name. Matches the existing mp_raise_NotImplementedError(NULL) convention used elsewhere in the codebase, including twice already in zephyr-cp's own Connection.c. The NotImplementedError message removal also fixes a CI failure: dropping that translatable string frees up enough flash to get bluemicro840's ja locale build back under its 524 KB limit (it was overflowing by 16 bytes).
… API The numeric-comparison pairing additions in shared-bindings/_bleio/Connection.c (authenticated, numeric_comparison, confirm_pairing) are compiled into every board with BLE enabled, adding a small fixed cost that pushed these already near-full boards over their flash budget in the ja locale build: circuitbrains_deluxe_m4 (+104B), datalore_ip_m4 (+124B), bluemicro840 (+144B), electronut_labs_blip (+80B). None of them need AES, so disable it to reclaim space, following the same approach used previously for electronut_labs_blip (74e380b) and matrixportal_m4 (c098474).
|
I want to leave a quick note about commit 41b619a. I'm not happy with the compromise it makes (disabling a crypto module from four boards' feature set) just to get the CI build to pass, but here's how I arrived at it (with the help of Claude):
Having said that, if there is an option for getting those boards to build that I haven't considered, I'm very open to implementing them. |
|
Other things we turn off to make space include
|
|
I wouldn't disable |
|
@copilot resolve the merge conflicts in this pull request |
…mcmp # Conflicts: # ports/nordic/boards/bluemicro840/mpconfigboard.mk # ports/nordic/boards/electronut_labs_blip/mpconfigboard.mk
…mp bleio API" This reverts commit 41b619a.
Motivation
Today a CircuitPython BLE peripheral can only do legacy Just Works pairing. Nothing surfaces a passkey or numeric-comparison value to Python, and the espressif port additionally raises
NotImplementedErrorfor any characteristic constructed with an*_WITH_MITMpermission. So a peripheral that has a display and a button — a lock, a medical device, anything handling data that shouldn't cross an unauthenticated link — has no way to require a man-in-the-middle-protected bond.This adds LE Secure Connections numeric comparison to
_bleio, implemented on the espressif port. NimBLE already ships the SC crypto (MYNEWT_VAL_BLE_SM_SC), so this is a configuration + event-plumbing change, not a crypto port.nordic,silabsandzephyr-cpget stubs (see Per-port status).New API —
shared-bindings/_bleio/Connectionauthenticated: boolTrueonce the link is MITM-protected (numeric comparison / passkey entry completed), as opposed to merely encrypted. Ports without an implementation always returnFalse.pairing_numeric_comparison: Optional[int]Noneotherwise. Non-blocking — the central drives the SM procedure, so unlikepair()this never blocks.confirm_pairing(accept: bool) -> Noneaccept=Falserejects and aborts pairing. RaisesConnectionErrorif the link dropped,_bleio.BluetoothErrorif nothing is pending.Numeric comparison is triggered automatically: constructing a
CharacteristicorDescriptorwithread_perm/write_perm==Attribute.LESC_ENCRYPT_WITH_MITMnow (a) maps to NimBLE's_AUTHENGATT flags instead of raising, and (b) tells the adapter to advertise numeric-comparison-capable IO. A central that then pairs to reach that characteristic gets the numeric-comparison flow; the peripheral pollspairing_numeric_comparison, shows the digits, and callsconfirm_pairing().Design: per-characteristic permission drives adapter SM config
The permission flags (layer 2, per-attribute enforcement) drive the adapter's SM configuration (layer 1, pairing negotiation), not the other way around:
common_hal_bleio_adapter_set_enabled()keeps the legacy defaults —sm_io_cap = NO_IO,sm_mitm = 0. Existing headless peripherals pair exactly as before.sm_sc = 1is now set unconditionally. It's negotiated per pairing (a peer that only does legacy pairing still works), it's strictly better crypto, and numeric comparison requires it.Characteristic/Descriptoris constructed with a*_WITH_MITMpermission,bleio_adapter_enable_mitm_pairing()raisesble_hs_cfg.sm_io_captoDISPLAY_YESNOandsm_mitmto1. This is a one-way, idempotent bump for the lifetime of the adapter.This avoids regressing headless espressif peripherals that pair with a central which negotiates MITM: an unconditional
DISPLAY_YESNOwould leave them with aPASSKEY_ACTIONthey can't answer and a 30 s SM timeout.espressif implementation
Characteristic.c/Descriptor.c: delete themp_raise_NotImplementedError("MITM security not supported")block; mapENC_WITH_MITM/LESC_ENC_WITH_MITM/SIGNED_*to_AUTHEN(NimBLE's GATT layer has no distinct LESC or signed flag).Connection.c: a newPAIR_WAITING_NUMCMPpairing state holds the value fromBLE_GAP_EVENT_PASSKEY_ACTION;confirm_pairing()injects the answer withble_sm_inject_io().BLE_GAP_EVENT_ENC_CHANGErecordsdesc.sec_state.authenticatedasConnection.authenticated. A passkey action this device can't service (INPUT/OOB) terminates the link withBLE_ERR_AUTH_FAILrather than stalling SM until its timeout.bleio_attribute_security_mode_requires_mitm()— small predicate inshared-module/_bleio/Attribute.c.Per-port status
get_authenticated()returnsFalse(SoftDevice LESC needs an app-side ECDH implementation that isn't currently vendored); the other two raiseNotImplementedError. Nordic is the natural next port — happy to sketch the plan.Not in scope / follow-ups
DISPLAY_ONLY/KEYBOARD_ONLY— only numeric comparison (DisplayYesNo) is handled; other IO capabilities are rejected.Connection.pair()is unchanged and doesn't drive numeric comparison — interactive pairing is inherently the non-blocking poll API.Testing
Peripheral-side usage looks like this — the only thing that opts a peripheral into numeric comparison is giving a characteristic the
LESC_ENCRYPT_WITH_MITMpermission:espressif_esp32s3_devkitc_1_n8r8;codeformat.py/codespell/extract_pyi.pyclean.pairing_numeric_comparisonmatches the phone's dialog →confirm_pairing(True)→authenticatedisTrue, MITM bond stored.authenticatedTrue, no new prompt.LESC_ENCRYPT_WITH_MITMcharacteristic: unreadable before the bond, readable after. Repeated connect/disconnect cycles crash-free.confirm_pairing(False)and abandoned-pairing paths: clean, no crash.Open questions for reviewers
Adapterproperty. Derivingsm_io_capfrom*_WITH_MITMcharacteristic perms is zero-config and safe-by-default, but it's a global side effect of constructing an attribute and it's sticky. Would an explicitadapter.io_capability = ...(or similar) be preferred, either instead or in addition (e.g. for a device that wants numeric comparison without an MITM characteristic)?shared-bindingsAPI, or should nordic be in this PR?confirm_pairing()raising vs. no-op on "nothing pending." Went with raising to match_bleionorms (__init__.craises"Already in progress"for the mirror case); the poll→confirm race is narrow and inside the exception envelope a real client already needs. Happy to switch to silent return if that's the house preference.Related
A separate issue: on this NimBLE build a client GATT procedure (
discover_remote_services()+ reads) running concurrently with an inbound SM pairing procedure on the same connection silently kills pairing —PASSKEY_ACTIONnever fires. Not caused by this change and worked around downstream by deferring the client read until after the bond, but worth an issue of its own.